﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace System
{
    internal static class UriHelper
    {
        public static string SpanToLowerInvariantString(ReadOnlySpan<char> span)
        {
            return string.Create(span.Length, span, static (buffer, span) =>
            {
                int charsWritten = span.ToLowerInvariant(buffer);
                Debug.Assert(charsWritten == buffer.Length);
            });
        }

        public static string NormalizeAndConcat(string? start, ReadOnlySpan<char> toNormalize)
        {
            var vsb = new ValueStringBuilder(stackalloc char[Uri.StackallocThreshold]);

            int charsWritten;
            while (!toNormalize.TryNormalize(vsb.RawChars, out charsWritten, NormalizationForm.FormC))
            {
                vsb.EnsureCapacity(vsb.Capacity + 1);
            }

            string result = string.Concat(start, vsb.RawChars.Slice(0, charsWritten));
            vsb.Dispose();
            return result;
        }

        // http://host/Path/Path/File?Query is the base of
        //      - http://host/Path/Path/File/ ...    (those "File" words may be different in semantic but anyway)
        //      - http://host/Path/Path/#Fragment
        //      - http://host/Path/Path/?Query
        //      - http://host/Path/Path/MoreDir/ ...
        //      - http://host/Path/Path/OtherFile?Query
        //      - http://host/Path/Path/Fl
        //      - http://host/Path/Path/
        //
        //  It is not a base for
        //      - http://host/Path/Path         (that last "Path" is not considered as a directory)
        //      - http://host/Path/Path?Query
        //      - http://host/Path/Path#Fragment
        //      - http://host/Path/Path2/
        //      - http://host/Path/Path2/MoreDir
        //      - http://host/Path/File
        //
        // ASSUMES that strings like http://host/Path/Path/MoreDir/../../  have been canonicalized before going to this method.
        // ASSUMES that back slashes already have been converted if applicable.
        //
        internal static bool TestForSubPath(ReadOnlySpan<char> self, ReadOnlySpan<char> other, bool ignoreCase)
        {
            int i = 0;
            char chSelf;
            char chOther;

            bool AllSameBeforeSlash = true;

            for (; i < self.Length && i < other.Length; ++i)
            {
                chSelf = self[i];
                chOther = other[i];

                if (chSelf == '?' || chSelf == '#')
                {
                    // survived so far and selfPtr does not have any more path segments
                    return true;
                }

                // If selfPtr terminates a path segment, so must otherPtr
                if (chSelf == '/')
                {
                    if (chOther != '/')
                    {
                        // comparison has failed
                        return false;
                    }
                    // plus the segments must be the same
                    if (!AllSameBeforeSlash)
                    {
                        // comparison has failed
                        return false;
                    }
                    //so far so good
                    AllSameBeforeSlash = true;
                    continue;
                }

                // if otherPtr terminates then selfPtr must not have any more path segments
                if (chOther == '?' || chOther == '#')
                {
                    break;
                }

                if (!ignoreCase)
                {
                    if (chSelf != chOther)
                    {
                        AllSameBeforeSlash = false;
                    }
                }
                else
                {
                    if (char.ToLowerInvariant(chSelf) != char.ToLowerInvariant(chOther))
                    {
                        AllSameBeforeSlash = false;
                    }
                }
            }

            // If self is longer then it must not have any more path segments
            for (; i < self.Length; ++i)
            {
                if ((chSelf = self[i]) == '?' || chSelf == '#')
                {
                    return true;
                }
                if (chSelf == '/')
                {
                    return false;
                }
            }
            //survived by getting to the end of selfPtr
            return true;
        }

        public static bool TryEscapeDataString(ReadOnlySpan<char> charsToEscape, Span<char> destination, out int charsWritten)
        {
            if (destination.Length < charsToEscape.Length)
            {
                charsWritten = 0;
                return false;
            }

            int indexOfFirstToEscape = charsToEscape.IndexOfAnyExcept(Unreserved);
            if (indexOfFirstToEscape < 0)
            {
                // Nothing to escape, just copy the original chars.
                charsToEscape.CopyTo(destination);
                charsWritten = charsToEscape.Length;
                return true;
            }

            // We may throw for very large inputs (when growing the ValueStringBuilder).
            scoped ValueStringBuilder vsb;

            // If the input and destination buffers overlap, we must take care not to overwrite parts of the input before we've processed it.
            bool overlapped = charsToEscape.Overlaps(destination);

            if (overlapped)
            {
                vsb = new ValueStringBuilder(stackalloc char[Uri.StackallocThreshold]);
                vsb.EnsureCapacity(charsToEscape.Length);
            }
            else
            {
                vsb = new ValueStringBuilder(destination.Slice(indexOfFirstToEscape));
            }

            EscapeStringToBuilder(charsToEscape.Slice(indexOfFirstToEscape), ref vsb, Unreserved, checkExistingEscaped: false);

            int newLength = checked(indexOfFirstToEscape + vsb.Length);
            Debug.Assert(newLength > charsToEscape.Length);

            if (destination.Length >= newLength)
            {
                charsToEscape.Slice(0, indexOfFirstToEscape).CopyTo(destination);

                if (overlapped)
                {
                    vsb.AsSpan().CopyTo(destination.Slice(indexOfFirstToEscape));
                    vsb.Dispose();
                }
                else
                {
                    // We are expecting the builder not to grow if the original span was large enough.
                    // This means that we MUST NOT over allocate anywhere in EscapeStringToBuilder (e.g. append and then decrease the length).
                    Debug.Assert(vsb.RawChars.Overlaps(destination));
                }

                charsWritten = newLength;
                return true;
            }

            vsb.Dispose();
            charsWritten = 0;
            return false;
        }

        public static string EscapeString(string stringToEscape, bool checkExistingEscaped, SearchValues<char> noEscape)
        {
            ArgumentNullException.ThrowIfNull(stringToEscape);

            return EscapeString(stringToEscape, checkExistingEscaped, noEscape, stringToEscape);
        }

        public static string EscapeString(ReadOnlySpan<char> charsToEscape, bool checkExistingEscaped, SearchValues<char> noEscape, string? backingString)
        {
            Debug.Assert(!noEscape.Contains('%'), "Need to treat % specially; it should be part of any escaped set");
            Debug.Assert(backingString is null || backingString.Length == charsToEscape.Length);

            int indexOfFirstToEscape = charsToEscape.IndexOfAnyExcept(noEscape);
            if (indexOfFirstToEscape < 0)
            {
                // Nothing to escape, just return the original value.
                return backingString ?? charsToEscape.ToString();
            }

            // Otherwise, create a ValueStringBuilder to store the escaped data into,
            // escape the rest, and concat the result with the characters we skipped above.
            var vsb = new ValueStringBuilder(stackalloc char[Uri.StackallocThreshold]);

            // We may throw for very large inputs (when growing the ValueStringBuilder).
            vsb.EnsureCapacity(charsToEscape.Length);

            EscapeStringToBuilder(charsToEscape.Slice(indexOfFirstToEscape), ref vsb, noEscape, checkExistingEscaped);

            string result = string.Concat(charsToEscape.Slice(0, indexOfFirstToEscape), vsb.AsSpan());
            vsb.Dispose();
            return result;
        }

        internal static void EscapeString(scoped ReadOnlySpan<char> stringToEscape, ref ValueStringBuilder dest,
            bool checkExistingEscaped, SearchValues<char> noEscape)
        {
            Debug.Assert(!noEscape.Contains('%'), "Need to treat % specially; it should be part of any escaped set");

            int indexOfFirstToEscape = stringToEscape.IndexOfAnyExcept(noEscape);
            if (indexOfFirstToEscape < 0)
            {
                // Nothing to escape, just copy the whole span.
                dest.Append(stringToEscape);
            }
            else
            {
                dest.Append(stringToEscape.Slice(0, indexOfFirstToEscape));

                EscapeStringToBuilder(stringToEscape.Slice(indexOfFirstToEscape), ref dest, noEscape, checkExistingEscaped);
            }
        }

        private static void EscapeStringToBuilder(
            scoped ReadOnlySpan<char> stringToEscape, ref ValueStringBuilder vsb,
            SearchValues<char> noEscape, bool checkExistingEscaped)
        {
            Debug.Assert(!stringToEscape.IsEmpty && !noEscape.Contains(stringToEscape[0]));

            // Allocate enough stack space to hold any Rune's UTF8 encoding.
            Span<byte> utf8Bytes = stackalloc byte[4];

            while (!stringToEscape.IsEmpty)
            {
                char c = stringToEscape[0];

                if (!char.IsAscii(c))
                {
                    if (Rune.DecodeFromUtf16(stringToEscape, out Rune r, out int charsConsumed) != OperationStatus.Done)
                    {
                        r = Rune.ReplacementChar;
                    }

                    Debug.Assert(stringToEscape.EnumerateRunes() is { } e && e.MoveNext() && e.Current == r);
                    Debug.Assert(charsConsumed is 1 or 2);

                    stringToEscape = stringToEscape.Slice(charsConsumed);

                    // The rune is non-ASCII, so encode it as UTF8, and escape each UTF8 byte.
                    r.TryEncodeToUtf8(utf8Bytes, out int bytesWritten);
                    foreach (byte b in utf8Bytes.Slice(0, bytesWritten))
                    {
                        PercentEncodeByte(b, ref vsb);
                    }

                    continue;
                }

                if (!noEscape.Contains(c))
                {
                    // If we're checking for existing escape sequences, then if this is the beginning of
                    // one, check the next two characters in the sequence.
                    if (c == '%' && checkExistingEscaped)
                    {
                        // If the next two characters are valid escaped ASCII, then just output them as-is.
                        if (stringToEscape.Length > 2 && char.IsAsciiHexDigit(stringToEscape[1]) && char.IsAsciiHexDigit(stringToEscape[2]))
                        {
                            vsb.Append('%');
                            vsb.Append(stringToEscape[1]);
                            vsb.Append(stringToEscape[2]);
                            stringToEscape = stringToEscape.Slice(3);
                            continue;
                        }
                    }

                    PercentEncodeByte((byte)c, ref vsb);
                    stringToEscape = stringToEscape.Slice(1);
                    continue;
                }

                // We have a character we don't want to escape. It's likely there are more, do a vectorized search.
                int charsToCopy = stringToEscape.IndexOfAnyExcept(noEscape);
                if (charsToCopy < 0)
                {
                    charsToCopy = stringToEscape.Length;
                }
                Debug.Assert(charsToCopy > 0);

                vsb.Append(stringToEscape.Slice(0, charsToCopy));
                stringToEscape = stringToEscape.Slice(charsToCopy);
            }
        }

        internal static void Unescape(scoped ReadOnlySpan<char> chars, ref ValueStringBuilder dest)
        {
            for (int i = 0; (uint)i < (uint)chars.Length;)
            {
                if (chars[i] == '%' && (uint)(i + 2) < (uint)chars.Length)
                {
                    char unescaped = DecodeHexChars(chars[i + 1], chars[i + 2]);

                    if (unescaped == Uri.c_DummyChar)
                    {
                        i++;
                        continue;
                    }

                    // Copy previous characters that don't require any transformations.
                    // Using a loop instead of Append(span) to avoid the call overhead for typically short sections.
                    foreach (char c in chars.Slice(0, i))
                    {
                        dest.Append(c);
                    }

                    if (char.IsAscii(unescaped))
                    {
                        dest.Append(unescaped);
                        i += 3;
                    }
                    else
                    {
                        int charactersRead = PercentEncodingHelper.UnescapePercentEncodedUTF8Sequence(
                            chars.Slice(i),
                            ref dest,
                            isQuery: false,
                            iriParsing: false);

                        Debug.Assert(charactersRead > 0);
                        i += charactersRead;
                    }

                    chars = chars.Slice(i);
                    i = 0;
                }
                else
                {
                    i++;
                }
            }

            dest.Append(chars);
        }

        internal static void UnescapeString(scoped ReadOnlySpan<char> chars, ref ValueStringBuilder dest,
            char rsvd1, char rsvd2, char rsvd3, UnescapeMode unescapeMode, UriParser? syntax, bool isQuery)
        {
            Debug.Assert(unescapeMode != UnescapeMode.None);

            bool escapeReserved = false;
            bool iriParsing = Uri.IriParsingStatic(syntax)
                                && ((unescapeMode & UnescapeMode.EscapeUnescape) == UnescapeMode.EscapeUnescape);

            while (!chars.IsEmpty)
            {
                int i;
                char ch = (char)0;

                for (i = 0; (uint)i < (uint)chars.Length; i++)
                {
                    ch = chars[i];

                    if (ch == '%')
                    {
                        if ((unescapeMode & UnescapeMode.Unescape) == 0)
                        {
                            // re-escape, don't check anything else
                            escapeReserved = true;
                        }
                        else if ((uint)(i + 2) < (uint)chars.Length)
                        {
                            ch = DecodeHexChars(chars[i + 1], chars[i + 2]);

                            // re-escape % from an invalid sequence
                            if (ch == Uri.c_DummyChar)
                            {
                                if ((unescapeMode & UnescapeMode.Escape) != 0)
                                    escapeReserved = true;
                                else
                                    continue;   // we should throw instead but since v1.0 would just print '%'
                            }
                            // Do not unescape '%' itself unless full unescape is requested
                            else if (ch == '%')
                            {
                                i += 2;
                                continue;
                            }
                            // Do not unescape a reserved char unless full unescape is requested
                            else if (ch == rsvd1 || ch == rsvd2 || ch == rsvd3)
                            {
                                i += 2;
                                continue;
                            }
                            // Do not unescape a dangerous char unless it's V1ToStringFlags mode
                            else if ((unescapeMode & UnescapeMode.V1ToStringFlag) == 0 && IsNotSafeForUnescape(ch))
                            {
                                i += 2;
                                continue;
                            }
                            else if (iriParsing && (ch <= '\x9F' ? IsNotSafeForUnescape(ch) : !IriHelper.CheckIriUnicodeRange(ch, isQuery)))
                            {
                                // check if unenscaping gives a char outside iri range
                                // if it does then keep it escaped
                                i += 2;
                                continue;
                            }
                            // unescape escaped char or escape %
                            break;
                        }
                        else
                        {
                            escapeReserved = true;
                        }
                        // escape (escapeReserved==true) or otherwise unescape the sequence
                        break;
                    }
                    else if ((unescapeMode & UnescapeMode.Escape) != 0)
                    {
                        // Could actually escape some of the characters
                        if (ch == rsvd1 || ch == rsvd2 || ch == rsvd3)
                        {
                            // found an unescaped reserved character -> escape it
                            escapeReserved = true;
                            break;
                        }
                        else if ((unescapeMode & UnescapeMode.V1ToStringFlag) == 0
                            && (ch <= '\x1F' || (ch >= '\x7F' && ch <= '\x9F')))
                        {
                            // found an unescaped reserved character -> escape it
                            escapeReserved = true;
                            break;
                        }
                    }
                }

                // Copy previous characters that don't require any transformations.
                // Using a loop instead of Append(span) to avoid the call overhead for typically short sections.
                foreach (char c in chars.Slice(0, i))
                {
                    dest.Append(c);
                }

                if (i < chars.Length)
                {
                    if (escapeReserved)
                    {
                        PercentEncodeByte((byte)chars[i], ref dest);
                        escapeReserved = false;
                        i++;
                    }
                    else if (ch <= 127)
                    {
                        dest.Append(ch);
                        i += 3;
                    }
                    else
                    {
                        // Unicode
                        int charactersRead = PercentEncodingHelper.UnescapePercentEncodedUTF8Sequence(
                            chars.Slice(i),
                            ref dest,
                            isQuery,
                            iriParsing);

                        Debug.Assert(charactersRead > 0);
                        i += charactersRead;
                    }
                }

                chars = chars.Slice(i);
            }
        }

        internal static void PercentEncodeByte(byte b, ref ValueStringBuilder to)
        {
            to.Append('%');
            HexConverter.ToCharsBuffer(b, to.AppendSpan(2), 0, HexConverter.Casing.Upper);
        }

        /// <summary>
        /// Converts 2 hex chars to a byte (returned in a char), e.g, "0a" becomes (char)0x0A.
        /// <para>If either char is not hex, returns <see cref="Uri.c_DummyChar"/>.</para>
        /// </summary>
        internal static char DecodeHexChars(int first, int second)
        {
            int a = HexConverter.FromChar(first);
            int b = HexConverter.FromChar(second);

            if ((a | b) == 0xFF)
            {
                // either a or b is 0xFF (invalid)
                return Uri.c_DummyChar;
            }

            return (char)((a << 4) | b);
        }

        // When unescaping in safe mode, do not unescape the RFC 3986 reserved set:
        // reserved    = gen-delims / sub-delims
        // gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
        // sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
        //             / "*" / "+" / "," / ";" / "="
        //
        // In addition, do not unescape the following unsafe characters:
        // excluded    = "%" / "\"
        internal static bool IsNotSafeForUnescape(char ch) =>
            s_notSafeForUnescapeChars.Contains(ch);

        private static readonly SearchValues<char> s_notSafeForUnescapeChars = SearchValues.Create(
            "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F" +
            "\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F" +
            ";/?:@&=+$,#[]!'()*" + "%\\" + "\u007F" +
            "\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F" +
            "\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F");

        /// <summary>All ASCII letters and digits, as well as the RFC3986 unreserved marks '-', '_', '.', and '~'.</summary>
        public static readonly SearchValues<char> Unreserved =
            SearchValues.Create("-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~");

        /// <summary>All ASCII letters and digits, as well as the RFC3986 reserved and unreserved marks.</summary>
        public static readonly SearchValues<char> UnreservedReserved =
            SearchValues.Create("!#$&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz~");

        public static readonly SearchValues<char> UnreservedReservedExceptHash =
            SearchValues.Create("!$&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz~");

        public static readonly SearchValues<char> UnreservedReservedExceptQuestionMarkHash =
            SearchValues.Create("!$&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz~");

        internal static readonly char[] s_WSchars = new char[] { ' ', '\n', '\r', '\t' };

        internal static bool IsLWS(char ch)
        {
            return (ch <= ' ') && (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t');
        }

        // Is this a Bidirectional control char.. These get stripped
        internal static bool IsBidiControlCharacter(char ch) =>
            char.IsBetween(ch, '\u200E', '\u202E') && !char.IsBetween(ch, '\u2010', '\u2029');

        // Strip Bidirectional control characters from this string
        public static string StripBidiControlCharacters(ReadOnlySpan<char> strToClean, string? backingString = null)
        {
            Debug.Assert(backingString is null || strToClean.Length == backingString.Length);

            if (StripBidiControlCharacters(strToClean, out string? stripped))
            {
                return stripped;
            }

            return backingString ?? strToClean.ToString();
        }

        public static bool StripBidiControlCharacters(ReadOnlySpan<char> strToClean, [NotNullWhen(true)] out string? stripped)
        {
            int charsToRemove = 0;

            int indexOfPossibleCharToRemove = strToClean.IndexOfAnyInRange('\u200E', '\u202E');
            if (indexOfPossibleCharToRemove >= 0)
            {
                // Slow path: Contains chars that fall in the [u200E, u202E] range (so likely Bidi)
                foreach (char c in strToClean.Slice(indexOfPossibleCharToRemove))
                {
                    if (IsBidiControlCharacter(c))
                    {
                        charsToRemove++;
                    }
                }
            }

            if (charsToRemove == 0)
            {
                // Hot path
                stripped = null;
                return false;
            }

            stripped = string.Create(strToClean.Length - charsToRemove, strToClean, static (buffer, strToClean) =>
            {
                int destIndex = 0;
                foreach (char c in strToClean)
                {
                    if (!IsBidiControlCharacter(c))
                    {
                        buffer[destIndex++] = c;
                    }
                }
                Debug.Assert(buffer.Length == destIndex);
            });
            return true;
        }

        // This will compress any "\" "/../" "/./" "///" "/..../" /XXX.../, etc found in the input
        //
        // The passed options control whether to use aggressive compression or the one specified in RFC 2396
        public static int Compress(Span<char> span, bool convertPathSlashes, bool canonicalizeAsFilePath)
        {
            if (span.IsEmpty)
            {
                return 0;
            }

            if (convertPathSlashes)
            {
                span.Replace('\\', '/');
            }

            ValueListBuilder<(int Start, int Length)> removedSegments = default;

            int slashCount = 0;
            int lastSlash = 0;
            int dotCount = 0;
            int removeSegments = 0;

            for (int i = span.Length - 1; i >= 0; i--)
            {
                char ch = span[i];

                // compress multiple '/' for file URI
                if (ch == '/')
                {
                    ++slashCount;
                }
                else
                {
                    if (slashCount > 1)
                    {
                        // else preserve repeated slashes
                        lastSlash = i + 1;
                    }
                    slashCount = 0;
                }

                if (ch == '.')
                {
                    ++dotCount;
                    continue;
                }
                else if (dotCount != 0)
                {
                    bool skipSegment = canonicalizeAsFilePath && (dotCount > 2 || ch != '/');

                    // Cases:
                    // /./                  = remove this segment
                    // /../                 = remove this segment, mark next for removal
                    // /....x               = DO NOT TOUCH, leave as is
                    // x.../                = DO NOT TOUCH, leave as is, except for V2 legacy mode
                    if (!skipSegment && ch == '/')
                    {
                        if ((lastSlash == i + dotCount + 1 // "/..../"
                                || (lastSlash == 0 && i + dotCount + 1 == span.Length)) // "/..."
                            && (dotCount <= 2))
                        {
                            //  /./ or /.<eos> or /../ or /..<eos>
                            removedSegments.Append((i + 1, dotCount + (lastSlash == 0 ? 0 : 1)));

                            lastSlash = i;
                            if (dotCount == 2)
                            {
                                // We have 2 dots in between like /../ or /..<eos>,
                                // Mark next segment for removal and remove this /../ or /..
                                ++removeSegments;
                            }
                            dotCount = 0;
                            continue;
                        }
                    }
                    // .NET 4.5 no longer removes trailing dots in a path segment x.../  or  x...<eos>
                    dotCount = 0;

                    // Here all other cases go such as
                    // x.[..]y or /.[..]x or (/x.[...][/] && removeSegments !=0)
                }

                // Now we may want to remove a segment because of previous /../
                if (ch == '/')
                {
                    if (removeSegments != 0)
                    {
                        removeSegments--;
                        removedSegments.Append((i + 1, lastSlash - i));
                    }

                    lastSlash = i;
                }
            }

            if (canonicalizeAsFilePath)
            {
                if (slashCount <= 1)
                {
                    if (removeSegments != 0 && span[0] != '/')
                    {
                        // remove first not rooted segment
                        removedSegments.Append((0, lastSlash + 1));
                    }
                    else if (dotCount != 0)
                    {
                        // If final string starts with a segment looking like .[...]/ or .[...]<eos>
                        // then we remove this first segment
                        if (lastSlash == dotCount || (lastSlash == 0 && dotCount == span.Length))
                        {
                            removedSegments.Append((0, dotCount + (lastSlash == 0 ? 0 : 1)));
                        }
                    }
                }
            }

            if (removedSegments.Length == 0)
            {
                return span.Length;
            }

            // Merge any remaining segments.
            // Write and read offsets are only ever the same for the first segment.
            // Copying the first section would no-op anyway, so we start with the first removed segment.
            int writeOffset = removedSegments[^1].Start;
            int readOffset = writeOffset;

            for (int i = removedSegments.Length - 1; i >= 0; i--)
            {
                (int start, int length) = removedSegments[i];

                Debug.Assert(start >= readOffset && length > 0 && start + length <= span.Length);

                if (readOffset != start)
                {
                    Debug.Assert(readOffset > writeOffset);

                    int segmentLength = start - readOffset;
                    span.Slice(readOffset, segmentLength).CopyTo(span.Slice(writeOffset));
                    writeOffset += segmentLength;
                }

                readOffset = start + length;
            }

            if (readOffset != span.Length)
            {
                Debug.Assert(readOffset > writeOffset);

                span.Slice(readOffset).CopyTo(span.Slice(writeOffset));
                writeOffset += span.Length - readOffset;
            }

            removedSegments.Dispose();
            return writeOffset;
        }
    }
}
